/*
 *  This file is part of the Image Metadata Editor.
 *
 *  Copyright (C) 2015 Australian Institute of Marine Science
 *
 *  Contact: Gael Lafond <g.lafond@aims.org.au>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package au.gov.aims.exif.tools;

import au.gov.aims.exif.editor.FXUtils;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Slider;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraintsBuilder;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.Imaging;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

// This class is multi-threaded (using Service and Task)
// See: http://docs.oracle.com/javafx/2/threads/jfxpub-threads.htm
public class ImageConverter extends Application {
	private static final Logger LOGGER = Logger.getLogger(ImageConverter.class.getName());

	// Application description displayed in the GUI.
	private static final String DESC = "This tool converts image files into JPEG format.\n" +
			"JPEG, also known as JPG, is a commonly used method of lossy compression for images. " +
			"Lossy mean that some information on the image is lost, reducing the image quality but " +
			"also reducing the image file size. The Image Metadata Editor only support JPEG images " +
			"since it's one of the only format that support metadata. To avoid lost of quality, " +
			"this tool convert images into the best JPEG quality, producing images that are often " +
			"larger than the original. The JPEG quality can be adjusted using the configuration " +
			"window, accessible from the menu File > Settings.\n" +
			"To convert your images into JPEG, choose the folder containing all your images using the " +
			"input folder button, choose where the JPEG images should be created using the output folder " +
			"button (you can choose the same folder as the input folder), and click the convert " +
			"button.";

	// Folder entered by the user - containing the image files to be converted to JPEG
	private File inputFolder = null;
	// List of all the image files found in the inputFolder
	private List<File> inputImageFiles = null;
	// Queue containing all the files that need to be converted. Threads pick files from that queue.
	// The queue is thread-safe, which ensure that no file will be converted by 2 thread at once.
	private ConcurrentLinkedQueue<File> fileQueue = null;
	// Folder entered by the user - converted JPEG are written in that folder
	private File outputFolder = null;

	// Label to display the number of files to convert AND the number of files converted
	private Label convertedImageLabel = new Label();

	// JavaFX folder chooser - single instance for better efficiency.
	private File fileChooserFolder = null;

	// Count the number of files that has been successfully converted to JPEG (used to show the summary at the end).
	// NOTE: AtomicInteger are thread safe (we can NOT use "++" on "int" property within a multi-threaded task)
	private AtomicInteger nbFileConverted = new AtomicInteger();
	// List of errors that occurred during the conversion process.
	// All errors are displayed at the end using the showSummary method.
	private List<String> processingErrors = null;

	// Configuration
	private float jpegQuality = 1.0f;
	private List<String> fileExtensions = null;

	public static void main(String[] args) {
		launch(args);
	}

	@Override
	public void start(final Stage primaryStage) {
		final ProgressBar progressBar = new ProgressBar(0);
		final Button progressCancel = new Button("Cancel");

		this.fileExtensions = new ArrayList<String>(2);
		this.fileExtensions.add("PNG");
		this.fileExtensions.add("GIF");

		// A service is a wrapper around a task. It allow a task to be re-used.
		final Service<Integer> service = new Service<Integer>() {
			@Override
			public void reset() {
				super.reset();

				ImageConverter.this.nbFileConverted.set(0);
				ImageConverter.this.processingErrors = new ArrayList<String>();

				ImageConverter.this.fileQueue = new ConcurrentLinkedQueue<File>(ImageConverter.this.inputImageFiles);

				// Bind the progress bar to the service just before starting it
				// (otherwise the progress bar will be in "waiting" mode until something happen)
				setupProgressBar(this, progressBar, progressCancel);
			}

			// This task is thread-safe, It can me fired multiple times at once to save execution time (but I haven't found how to fire multiple instance)
			protected Task<Integer> createTask() {
				// Defining the task that will be executed by the application:
				//     This is run by an independent thread (or multi-threads), using a Service object
				return new Task<Integer>() {
					@Override
					protected Integer call() throws Exception {
						File imageFile;
						int iteration = 0;
						while ((imageFile = ImageConverter.this.fileQueue.poll()) != null) {
							if (isCancelled()) {
								updateMessage("Cancelled");
								break;
							}

							try {
								ImageConverter.convertImage(
										imageFile,
										ImageConverter.this.inputFolder,
										ImageConverter.this.outputFolder,
										ImageConverter.this.jpegQuality);
								ImageConverter.this.nbFileConverted.incrementAndGet();
							} catch (OutOfMemoryError ex) {
									// NOTE: We have enough memory to log the error since the memory used by
									//     the image has been freed after the exception occurred.
									ImageConverter.this.processingErrors.add(
											"Not enough memory to process the file: " +
											imageFile.getAbsolutePath());
									LOGGER.log(Level.SEVERE,
											"Not enough memory to process the file: " +
											imageFile.getAbsolutePath(), ex);
							} catch (Exception ex) {
								if (isCancelled()) {
									ImageConverter.this.processingErrors.add("Operation cancelled");
									LOGGER.log(Level.SEVERE, "Operation cancelled", ex);
								} else {
									ImageConverter.this.processingErrors.add(
											"Exception occurred while processing the file: " +
											imageFile.getAbsolutePath() + " " + ex.getMessage());
									LOGGER.log(Level.SEVERE,
											"Exception occurred while processing the file: " +
											imageFile.getAbsolutePath(), ex);
								}
							}

							iteration++;
							updateMessage("Iteration " + iteration);
							int done = ImageConverter.this.inputImageFiles.size() - ImageConverter.this.fileQueue.size();
							updateProgress(done, ImageConverter.this.inputImageFiles.size());
						}
						return iteration;
					}
				};
			}
		};

		// Update the "Progress label" as the task progress (Something like "12 / 210")
		service.progressProperty().addListener(new ChangeListener<Number>() {
			@Override
			public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
				if (ImageConverter.this.fileQueue == null || ImageConverter.this.inputImageFiles == null) {
					convertedImageLabel.setText("");
				} else {
					int done = ImageConverter.this.inputImageFiles.size() - ImageConverter.this.fileQueue.size();
					convertedImageLabel.setText(done + " / " + ImageConverter.this.inputImageFiles.size());
				}
			}
		});

		// Unbind after success
		service.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
			@Override
			public void handle(WorkerStateEvent event) {
				ImageConverter.this.showSummary();
				resetProgressBar(progressBar, progressCancel);
			}
		});

		// Unbind and stop on cancel
		service.setOnCancelled(new EventHandler<WorkerStateEvent>() {
			@Override
			public void handle(WorkerStateEvent event) {
				ImageConverter.this.showSummary();
				resetProgressBar(progressBar, progressCancel);
			}
		});

		// Unbind and stop on failure
		service.setOnFailed(new EventHandler<WorkerStateEvent>() {
			@Override
			public void handle(WorkerStateEvent event) {
				FXUtils.getNotificationWindow().showError("An error occurred while converting the images.");
				LOGGER.log(Level.SEVERE, "Unexpected error occurred");
				ImageConverter.this.showSummary();
				resetProgressBar(progressBar, progressCancel);
			}
		});


		// Definition of the GUI
		primaryStage.setTitle("Image converter");
		primaryStage.setMinWidth(500);
		primaryStage.setMinHeight(300);
		primaryStage.setWidth(700);
		primaryStage.setHeight(450);
		primaryStage.getIcons().add(FXUtils.getResourceImage("icon.png"));

		MenuBar menuBar = new MenuBar();

		// #############
		// # FILE MENU #
		// #############

		Menu menuFile = new Menu("File");

		MenuItem configItem = new MenuItem("Settings...");
		configItem.setOnAction(new EventHandler<ActionEvent>() {
			private Stage configWindow = null;

			@Override
			public void handle(ActionEvent event) {
				this.configWindow = new Stage();
				this.configWindow.setTitle("Image converter settings");
				this.configWindow.getIcons().add(FXUtils.getResourceImage("icon.png"));
				this.configWindow.initModality(Modality.APPLICATION_MODAL);
				this.configWindow.setMinWidth(400);
				this.configWindow.setWidth(600);
				this.configWindow.setMinHeight(170);
				this.configWindow.setHeight(170);

				this.configWindow.onCloseRequestProperty().set(new EventHandler<WindowEvent>() {
					public void handle(WindowEvent e) {
						// abort action and close the dialog.
						configWindow.close();
						configWindow = null;
					}
				});

				BorderPane configBody = new BorderPane();

				GridPane configGrid = new GridPane();
				configGrid.getColumnConstraints().addAll(
						ColumnConstraintsBuilder.create().minWidth(150).build(),
						ColumnConstraintsBuilder.create().hgrow(Priority.ALWAYS).build());
				configGrid.setPadding(new Insets(10, 10, 10, 10));

				// Define config elements

				// JPEG Quality slider
				int row = 0;
				final Slider jpegQualitySlider = new Slider();
				jpegQualitySlider.setMin(0);
				jpegQualitySlider.setMax(1);
				jpegQualitySlider.setValue(ImageConverter.this.jpegQuality);
				jpegQualitySlider.setShowTickLabels(true);
				jpegQualitySlider.setShowTickMarks(true);
				jpegQualitySlider.setMajorTickUnit(0.25);
				final Label jpegQualityValue = new Label(String.format("%.2f", jpegQualitySlider.getValue()));

				jpegQualitySlider.valueProperty().addListener(new ChangeListener<Number>() {
					public void changed(ObservableValue<? extends Number> value, Number oldValue, Number newValue) {
						jpegQualityValue.setText(String.format("%.2f", newValue.floatValue()));
					}
				});

				HBox jpegQualityBox = new HBox();
				jpegQualityBox.setSpacing(5);
				HBox.setHgrow(jpegQualitySlider, Priority.ALWAYS);
				jpegQualityBox.getChildren().addAll(jpegQualitySlider, jpegQualityValue);

				//jpegQualityField.setPromptText("Enter the JPEG quality. 1 for best quality, 0 for smallest file size.");
				Label jpegQualityLabel = new Label("JPEG Quality");
				// NOTE: Label.setLabelFor seems to only add semantic value.
				jpegQualityLabel.setLabelFor(jpegQualitySlider);
				GridPane.setConstraints(jpegQualityLabel, 0, row);
				GridPane.setConstraints(jpegQualityBox, 1, row);
				GridPane.setValignment(jpegQualityLabel, VPos.TOP);
				configGrid.getChildren().addAll(jpegQualityLabel, jpegQualityBox);

				// Image extensions checkboxes
				row++;
				VBox fileExtCheckboxes = new VBox();
				fileExtCheckboxes.setSpacing(5);

				final CheckBox fileExtPNG = new CheckBox("PNG");
				fileExtPNG.setSelected(ImageConverter.this.fileExtensions.contains("PNG"));

				final CheckBox fileExtGIF = new CheckBox("GIF");
				fileExtGIF.setSelected(ImageConverter.this.fileExtensions.contains("GIF"));

				fileExtCheckboxes.getChildren().addAll(fileExtPNG, fileExtGIF);

				Label fileExtLabel = new Label("Image file types");
				fileExtLabel.setLabelFor(fileExtCheckboxes);
				GridPane.setConstraints(fileExtLabel, 0, row);
				GridPane.setConstraints(fileExtCheckboxes, 1, row);
				GridPane.setValignment(fileExtLabel, VPos.TOP);
				configGrid.getChildren().addAll(fileExtLabel, fileExtCheckboxes);


				configBody.setCenter(configGrid);

				Button saveButton = new Button("Save");
				saveButton.setDefaultButton(true); // Trigger when ENTER is pressed
				saveButton.setOnAction(new EventHandler<ActionEvent>() {
					@Override
					public void handle(ActionEvent actionEvent) {
						saveConfig(
								(float)Math.round(jpegQualitySlider.getValue() * 100) / 100,
								fileExtPNG, fileExtGIF
						);
						configWindow.close();
						configWindow = null;
					}
				});

				Button cancelButton = new Button("Cancel");
				cancelButton.setCancelButton(true); // Trigger when ESC is pressed
				cancelButton.setOnAction(new EventHandler<ActionEvent>() {
					@Override
					public void handle(ActionEvent actionEvent) {
						configWindow.close();
						configWindow = null;
					}
				});

				HBox buttonBox = new HBox();
				buttonBox.setSpacing(15);
				buttonBox.setPadding(new Insets(10, 10, 10, 10));
				buttonBox.setAlignment(Pos.CENTER_RIGHT);
				buttonBox.getChildren().addAll(saveButton, cancelButton);

				configBody.setBottom(buttonBox);

				this.configWindow.setScene(new Scene(configBody));
				this.configWindow.show();
			}

			private void saveConfig(float jpegQuality, CheckBox... fileTypes) {
				ImageConverter.this.jpegQuality = jpegQuality;

				ImageConverter.this.fileExtensions = new ArrayList<String>(fileTypes.length);
				for (CheckBox fileType : fileTypes) {
					if (fileType.isSelected()) {
						ImageConverter.this.fileExtensions.add(fileType.getText());
					}
				}

				// Find image files that need to be converted into JPEG (the list may have changed since the extensions changes)
				ImageConverter.this.loadListFilesToConvert();
			}
		});
		menuFile.getItems().addAll(configItem);

		menuBar.getMenus().addAll(menuFile);

		BorderPane body = new BorderPane();

		// Put the menu and the application header in the top of the border panel
		VBox vbox = new VBox();
		vbox.getChildren().addAll(menuBar, FXUtils.createApplicationHeader());
		body.setTop(vbox);

		GridPane grid = new GridPane();
		grid.getColumnConstraints().addAll(
				ColumnConstraintsBuilder.create().minWidth(150).build(),
				ColumnConstraintsBuilder.create().hgrow(Priority.ALWAYS).build());
		grid.setPadding(new Insets(10, 10, 10, 10));
		grid.setVgap(5);
		grid.setHgap(5);


		// ###############
		// # Description #
		// ###############

		int row = 0;
		Label description = new Label(DESC);
		description.setWrapText(true);

		GridPane.setColumnSpan(description, 2);
		GridPane.setConstraints(description, 0, row, 2, 1);
		grid.getChildren().add(description);


		// #######################
		// # Input folder button #
		// #######################

		row++;
		final Label inputFolderLabel = new Label();
		Button inputButton = new Button("Input folder");
		inputButton.setOnAction(
			new EventHandler<ActionEvent>() {
				@Override
				public void handle(final ActionEvent e) {
					DirectoryChooser folderChooser = new DirectoryChooser();
					ImageConverter.this.setDirectoryChooserFolder(folderChooser, ImageConverter.this.inputFolder);

					File folder = folderChooser.showDialog(primaryStage);
					if (folder != null) {
						ImageConverter.this.inputFolder = folder;
						ImageConverter.this.setFileChooserFolder(ImageConverter.this.inputFolder);
						// Put folder path into display field
						inputFolderLabel.setText(ImageConverter.this.inputFolder.getAbsolutePath());
						// Find image files that need to be converted to JPEG
						ImageConverter.this.loadListFilesToConvert();
					}
				}
			}
		);

		GridPane.setConstraints(inputButton, 0, row, 2, 1);
		GridPane.setMargin(inputButton, new Insets(0, 10, 0, 0));

		GridPane.setConstraints(inputFolderLabel, 1, row, 2, 1);
		GridPane.setMargin(inputFolderLabel, new Insets(0, 0, 0, 0));

		grid.getChildren().addAll(inputButton, inputFolderLabel);


		// ########################
		// # Output folder button #
		// ########################

		row++;
		final Label outputFolderLabel = new Label();
		Button outputButton = new Button("Output folder");
		outputButton.setOnAction(
			new EventHandler<ActionEvent>() {
				@Override
				public void handle(final ActionEvent e) {
					DirectoryChooser folderChooser = new DirectoryChooser();
					ImageConverter.this.setDirectoryChooserFolder(folderChooser, ImageConverter.this.outputFolder);

					File folder = folderChooser.showDialog(primaryStage);
					if (folder != null) {
						ImageConverter.this.outputFolder = folder;
						ImageConverter.this.setFileChooserFolder(ImageConverter.this.outputFolder);
						// Put folder path into display field
						outputFolderLabel.setText(ImageConverter.this.outputFolder.getAbsolutePath());
					}
				}
			}
		);

		GridPane.setConstraints(outputButton, 0, row, 2, 1);
		GridPane.setMargin(outputButton, new Insets(10, 10, 0, 0));

		GridPane.setConstraints(outputFolderLabel, 1, row, 2, 1);
		GridPane.setMargin(outputFolderLabel, new Insets(10, 0, 0, 0));

		grid.getChildren().addAll(outputButton, outputFolderLabel);


		// ##################
		// # Convert button #
		// ##################

		row++;
		Button convertButton = new Button("Convert");
		convertButton.setOnAction(
			new EventHandler<ActionEvent>() {
				@Override
				public void handle(final ActionEvent e) {
					if (ImageConverter.this.inputFolder == null || ImageConverter.this.outputFolder == null) {
						if (ImageConverter.this.inputFolder == null) {
							FXUtils.getNotificationWindow().showError("You must provide an input folder.");
						}
						if (ImageConverter.this.outputFolder == null) {
							FXUtils.getNotificationWindow().showError("You must provide an output folder.");
						}
					} else {
						if (!service.isRunning()) {
							service.reset();
							service.start();
						}
					}
				}
			}
		);

		GridPane.setConstraints(convertButton, 0, row, 2, 1);
		GridPane.setMargin(convertButton, new Insets(10, 0, 0, 0));

		GridPane.setConstraints(convertedImageLabel, 1, row, 2, 1);
		GridPane.setMargin(convertedImageLabel, new Insets(10, 0, 0, 0));

		grid.getChildren().addAll(convertButton, convertedImageLabel);


		// ################
		// # Progress bar #
		// ################

		HBox progressRow = new HBox(10); // 10 = element spacing
		progressRow.setPadding(new Insets(0, 0, 10, 0));
		progressRow.setAlignment(Pos.CENTER);

		progressBar.prefWidthProperty().bind(grid.widthProperty().subtract(110));

		progressCancel.setDisable(true);
		progressCancel.setOnAction(
				new EventHandler<ActionEvent>() {
					@Override
					public void handle(final ActionEvent e) {
						service.cancel();
					}
				}
		);
		progressRow.getChildren().addAll(progressBar, progressCancel);

		body.setCenter(grid);
		body.setBottom(progressRow);

		Scene scene = new Scene(body);

		primaryStage.setScene(scene);

		primaryStage.show();
	}

	private void setDirectoryChooserFolder(DirectoryChooser folderChooser, File previousValue) {
		File validFolder = null;
		if (previousValue != null) {
			validFolder = previousValue;
		} else if (this.fileChooserFolder != null) {
			validFolder = this.fileChooserFolder;
		}

		if (validFolder != null) {
			while (!validFolder.exists()) {
				validFolder = validFolder.getParentFile();
			}
			folderChooser.setInitialDirectory(validFolder);
		}
	}

	private void loadListFilesToConvert() {
		if (this.inputFolder != null) {
			// Find image files that need to be converted to JPEG
			this.inputImageFiles = findFiles(this.inputFolder, this.fileExtensions);
			this.convertedImageLabel.setText(this.inputImageFiles.size() + " image files to convert");
		}
	}

	private void setFileChooserFolder(File chosenFile) {
		if (chosenFile != null) {
			File folder = chosenFile.getParentFile();
			if (folder != null) {
				this.fileChooserFolder = folder;
			}
		}
	}

	private void setupProgressBar(Service service, ProgressBar progressBar, Button cancel) {
		progressBar.progressProperty().bind(service.progressProperty());
		cancel.setDisable(false);
	}

	private void resetProgressBar(ProgressBar progressBar, Button cancel) {
		progressBar.progressProperty().unbind();
		progressBar.setProgress(0);
		cancel.setDisable(true);
	}

	private void showSummary() {
		int nbConverted = this.nbFileConverted.intValue();

		FXUtils.getNotificationWindow().showMessage(nbConverted + " image files successfully converted to JPEG.");
		if (nbConverted < this.inputImageFiles.size()) {
			FXUtils.getNotificationWindow().showWarning((this.inputImageFiles.size() - nbConverted) + " image files could not be converted.");
		}
		if (!this.processingErrors.isEmpty()) {
			for (String errorMsg : this.processingErrors) {
				FXUtils.getNotificationWindow().showError(errorMsg);
			}
		}
	}

	/**
	 * PROCESSING
	 */

	// This is the actual processing. Everything else is GUI elements and threads handling.
	private static void convertImage(File source, File inputFolder, File outputFolder, float jpegQuality) throws IOException, ImageReadException {
		// Compute the destination file (same folder structure as the source image, in the destination folder)
		String sourceFilename = source.getName();
		String sourceFilenameWithoutExtension = sourceFilename.contains(".") ? sourceFilename.substring(0, sourceFilename.lastIndexOf('.')) : sourceFilename;

		URI sourceRelativeURI = inputFolder.toURI().relativize(source.getParentFile().toURI());
		File destination = new File(outputFolder, sourceRelativeURI.getPath() + sourceFilenameWithoutExtension + ".jpg");

		// Find a file that doesn't exists, to ensure no file are overwritten.
		int counter = 0;
		while (destination.exists()) {
			counter++;
			destination = new File(outputFolder, sourceRelativeURI.getPath() + sourceFilenameWithoutExtension + "_" + counter + ".jpg");
		}

		// Create the destination folder if it doesn't exists (the next comment is to suppress the IDE warning about "mkdirs" output)
		//noinspection ResultOfMethodCallIgnored
		destination.getParentFile().mkdirs();

		// Convert the image files into JPEG using ImageIO
		BufferedImage image = Imaging.getBufferedImage(source);

		// NOTE: Apache imaging throw a ImageWriteException when trying the write an image as JPEG:
		//     org.apache.commons.imaging.ImageWriteException: This image format (Jpeg-Custom) cannot be written.

		// *IMPORTANT* ImageIO is not wise enough to create a JPEG from an image with an Alpha channel (an image with transparency)
		//     i.e. This line won't work: ImageIO.write(image, "jpg", destination);
		//     Solution: Create a new empty image and write the image in it.
		// See: http://www.mkyong.com/java/convert-png-to-jpeg-image-file-in-java/

		// Create a blank RGB image with the same width and height as the original image
		BufferedImage newBufferedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);

		// Write the original image into the new image, on a white background (this operation get rid of the Alpha channel)
		newBufferedImage.createGraphics().drawImage(image, 0, 0, Color.WHITE, null);

		// Write the image to disk as a JPEG image.
		ImageConverter.writeJpegImage(newBufferedImage, destination, jpegQuality);

		// Free memory
		newBufferedImage.flush();
		image.flush();
	}

	// NOTE: This method simply write the image to disk, using the default JPEG writer...
	//     Normally, it should be done with a single line of code:
	//         ImageIO.write(newBufferedImage, "jpg", destination);
	//     but we need to set the compression level so the writer has to be cut open
	//     and tweaked.
	private static void writeJpegImage(BufferedImage image, File destination, float compression) throws IOException {
		// Get the default JPEG writer
		ImageWriter jpegWriter = ImageIO.getImageWritersByFormatName("jpg").next();
		// Get the JPEG writer params
		ImageWriteParam jpegWriteParam = jpegWriter.getDefaultWriteParam();
		// Set the JPEG compression level
		jpegWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
		jpegWriteParam.setCompressionQuality(compression);

		// Create an output stream and write the output using the tweaked JPEG writer.
		ImageOutputStream outputStream = null;
		try {
			outputStream = new FileImageOutputStream(destination);
			jpegWriter.setOutput(outputStream);
			IIOImage outputImage = new IIOImage(image, null, null);
			jpegWriter.write(null, outputImage, jpegWriteParam);
		} finally {
			// Get rid of the tweaked writer and close the output streams
			jpegWriter.dispose();
			if (outputStream != null) {
				outputStream.close();
			}
		}
	}

	// NOTE: Can not use File.listFiles(FilenameFilter): "listFiles" is not recursive
	private static List<File> findFiles(final File folder, final List<String> fileExtensions) {
		List<File> foundFiles = new ArrayList<File>();
		File[] files = folder.listFiles();
		if (files != null && files.length > 0) {
			for (File file : files) {
				if (file.isDirectory()) {
					foundFiles.addAll(findFiles(file, fileExtensions));
				} else {
					String filename = file.getName();
					for (String fileExtension : fileExtensions) {
						if (filename.toLowerCase().endsWith("." + fileExtension.toLowerCase())) {
							foundFiles.add(file);
						}
					}
				}
			}
		}
		return foundFiles;
	}
}
